Offer a footer Cancel on a stepper command dialog - #129
Conversation
The X in the header was the only way out of a wizard, which is easy to miss on a long or destructive flow and disappears entirely when the dialog is presented without a visible header. The cancel path itself already existed - the X routes through it - so this only surfaces it as a button. It is opt-in, because turning it on by default would silently re-lay-out every wizard already in use. The button sits leftmost, before Previous and outside the spacer, so it stays in the same place when Previous appears on the second step rather than sliding across as the footer fills. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
Asserting that a button with the right label renders proves almost nothing here: the dialog already has a close path, and a cancel that quietly closed as confirmed would look identical in static markup. These specs run in a DOM, click the real button, and assert which arm ran - the cancel callback fired, the confirm callback did not, and the dialog context closed as cancelled. A control spec drives submit through the same harness, so "confirm never ran" is falsifiable rather than assumed. Position is asserted as footer layout including the spacer, not as button order. Order alone cannot tell "before the spacer" from "after it" on the first step, where there is no Previous to sit between - which is the one cell where the claim that cancel stays put actually needed proving. Busy is asserted behaviorally: a click during execution is ignored, and honored again once it finishes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
|
Reviewer context — resolves #121. Commits The cancel path already existed
Position: leftmost, before Previous and outside the Why the specs run in a DOMThe repo default is The load-bearing assertion is which arm ran: VerificationOrchestrator re-ran independently: 12 mutations, each Three things stated plainly rather than papered overM10 is a multi-line mutation. No single-line edit moves Cancel relative to Previous — it takes a 9-line block move. The step-dependence axis is covered single-line (M5, M6); pure reordering is not. A real gap was found and closed mid-way. Button order alone cannot distinguish "before the spacer" from "after it" on the first step, where there is no Previous between them — exactly the cell where the position claim needed proving. The layout helper now describes the spacer, and every mutation whose target specs changed was re-run. One assertion is only non-vacuous by proximity. Not pinned
A candidate vacuity species — the obedient doubleEvery spec in this folder mocks Distinct from "asserting through a blind interface" (which never calls the real thing) and from "a downstream stage launders the defect" (where the real downstream refuses the bad value): here the downstream is replaced by a more obedient one, so the spec grades the mock's contract rather than the library's. Any mocked-boundary component spec inherits it. The honest scope of these specs is "the dialog asks for the right thing" — not "the shipped button obeys." |
The footer Cancel was disabled once the command was executing, but the header close and Escape were not, and the busy flag was only raised after awaiting a consumer's onBeforeExecute transform. An async transform therefore left a window in which the command was already committed to running while Cancel was still live: the operator cancelled, the dialog closed reporting cancellation, and the write landed anyway. Raise the busy flag as the first statement of the submit path and gate the header close and Escape on it, so the dialog stops offering an outcome it cannot deliver. The existing "ignore a cancel click while the command runs" assertion could not fail. jsdom returns early from click() on a disabled control, so it only ever observed the disabled attribute -- the same fact its sibling asserts -- and deleting the click handler outright left the second suite green. That suite now settles the run and clicks again, so the handler is pinned where it is used, and the misleading assertion is renamed to what it measures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
showCancel and cancelLabel shipped as public props with no documentation, and the surrounding prose still claimed cancel is always available via the header X -- which was never the whole truth and is now wrong in a second way, since no dismissal route is offered while the command runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
|
Reviewer context for the two commits just pushed. Two fresh independent reviewers (code and security) looked at this branch; both found merge-blocking problems, and this addresses all of them. The spec that could not fail
The second suite made it worse: it carried that assertion with no "honored once it returns" counterpart, so deleting the Fixed by settling the run and clicking again inside the second suite, and renaming the overclaiming assertion to Independently re-verified before merge. Baseline: 11/11 green. Deleting Every new assertion, and the single line it dies to
Both regressions are caught: restoring the old One line I could not independently falsify — stating it rather than hiding it
It was kept deliberately: it states intent and holds the Escape path on its own terms if Gates
Note: Adjacent, deliberately out of scope
Not verifiedNo browser was driven — the evidence is jsdom plus the mutation runs above. The async-transform window was reproduced in a spec, not observed against a real slow command. |
Added
StepperCommandDialogcan show an opt-in Cancel action in its footer, with a configurable label (StepperCommandDialog has no footer Cancel action #121)Fixed
StepperCommandDialogno longer offers to cancel a command it has already committed to running, so an operator can no longer be told a write was cancelled while it landsEscapeare withdrawn while aStepperCommandDialogcommand is running, matching the footer Cancel